# frozen_string_literal: true

require_relative '../../../spec_helper'
require 'wpxf/modules'

describe Wpxf::Module do
  let(:subject) { Wpxf::Module.new }

  let(:subject_with_auth) do
    Class.new(Wpxf::Module) do
      def requires_authentication
        true
      end
    end.new
  end

  before :each, 'setup subject' do
    subject.active_workspace = Wpxf::Models::Workspace.first
    subject.set_option_value('host', '127.0.0.1')
  end

  describe '#new' do
    it 'registers the verbose option' do
      expect(subject.get_option('verbose')).to_not be_nil
    end

    context 'when requires_authentication is true' do
      it 'registers the username and password options' do
        expect(subject_with_auth.get_option('username')).to_not be_nil
        expect(subject_with_auth.get_option('password')).to_not be_nil
      end
    end

    context 'when requires_authentication is false' do
      it 'does not register the username and password options' do
        expect(subject.get_option('username')).to be_nil
        expect(subject.get_option('password')).to be_nil
      end
    end
  end

  describe '#unset_option' do
    it 'unsets module and payload options matching the specified name' do
      opt = Wpxf::StringOption.new(name: 'foo', desc: 'desc')
      subject.register_option(opt)
      subject.set_option_value(opt.name, 'bar')

      subject.payload = Wpxf::Payload.new
      subject.payload.register_option(opt)
      subject.payload.set_option_value(opt.name, 'bar')

      expect(subject.option_value?(opt.name)).to be true
      expect(subject.payload.option_value?(opt.name)).to be true

      subject.unset_option(opt.name)
      expect(subject.option_value?(opt.name)).to be false
      expect(subject.payload.option_value?(opt.name)).to be false
    end
  end

  describe '#set_option_value' do
    it 'returns :not_found if an invalid option is specified' do
      expect(subject.set_option_value('opt1', 'val')).to eq :not_found
    end

    context 'when the option only exists in the module' do
      it 'returns :invalid if the value isn\'t valid' do
        opt = Wpxf::BooleanOption.new(name: 'opt1', desc: 'opt1')
        subject.register_option(opt)
        expect(subject.set_option_value('opt1', 'invalid')).to eq :invalid
      end

      it 'returns the normalized value if the value was set' do
        opt = Wpxf::BooleanOption.new(name: 'opt1', desc: 'opt1')
        subject.register_option(opt)
        expect(subject.set_option_value('opt1', 't')).to eq true
      end

      it 'sets the value in datastore to the value specified' do
        opt = Wpxf::BooleanOption.new(name: 'opt1', desc: 'opt1')
        subject.register_option(opt)
        expect(subject.set_option_value('opt1', 't')).to eq true
        expect(subject.datastore['opt1']).to eq 't'
      end
    end

    context 'when the option exists in the payload' do
      it 'returns :invalid if the value isn\'t valid' do
        subject.payload = Wpxf::Payload.new
        opt = Wpxf::BooleanOption.new(name: 'opt1', desc: 'opt1')
        subject.payload.register_option(opt)
        expect(subject.set_option_value('opt1', 'invalid')).to eq :invalid
      end

      it 'returns the normalized value if the value was set' do
        subject.payload = Wpxf::Payload.new
        opt = Wpxf::BooleanOption.new(name: 'opt1', desc: 'opt1')
        subject.payload.register_option(opt)
        expect(subject.set_option_value('opt1', 't')).to eq true
      end

      it 'sets the value in datastore to the value specified' do
        subject.payload = Wpxf::Payload.new
        opt = Wpxf::BooleanOption.new(name: 'opt1', desc: 'opt1')
        subject.payload.register_option(opt)
        expect(subject.set_option_value('opt1', 't')).to eq true
        expect(subject.payload.datastore['opt1']).to eq 't'
      end
    end
  end

  describe '#missing_options' do
    it 'returns an array of option names that are required and empty' do
      opt_a = Wpxf::StringOption.new(
        name: 'opt_a',
        desc: 'desc',
        required: true
      )

      opt_b = Wpxf::StringOption.new(
        name: 'opt_b',
        desc: 'desc',
        required: true
      )

      opt_c = Wpxf::StringOption.new(
        name: 'opt_c',
        desc: 'desc'
      )

      subject.payload = Wpxf::Payload.new
      subject.register_options([opt_a, opt_c])
      subject.payload.register_option(opt_b)

      expect(subject.missing_options).to eq %w[opt_a opt_b]
    end
  end

  describe '#aux_module?' do
    it 'returns true if a module is in the Auxiliary namespace' do
      mod = Wpxf::Auxiliary::AllInOneMigrationExport.new
      expect(mod.aux_module?).to be true
    end
  end

  describe '#exploit_module?' do
    it 'returns true if a module is in the Exploit namespace' do
      mod = Wpxf::Exploit::AdminShellUpload.new
      expect(mod.exploit_module?).to be true
    end
  end

  describe '#authenticate_with_wordpress' do
    it 'emits a message indicating the module is authenticating' do
      events_emitted = 1
      allow(subject).to receive(:on_event_emitted) do |event|
        if events_emitted == 1
          expect(event[:msg]).to match(/Authenticating with WordPress/)
          expect(event[:event]).to eq :output
          expect(event[:type]).to eq :info
        end

        events_emitted += 1
      end

      allow(subject).to receive(:wordpress_login).and_return('cookie')
      subject.event_emitter.subscribe(subject)
      subject.authenticate_with_wordpress('user', 'pass')
    end

    context 'when successfully authenticated' do
      it 'emits a message indicating the module authenticated' do
        events_emitted = 1
        allow(subject).to receive(:on_event_emitted) do |event|
          if events_emitted == 2
            expect(event[:msg]).to match(/Authenticated with WordPress/)
            expect(event[:event]).to eq :output
            expect(event[:type]).to eq :success
            expect(event[:verbose]).to be true
          end

          events_emitted += 1
        end

        allow(subject).to receive(:wordpress_login).and_return('cookie')
        subject.event_emitter.subscribe(subject)
        subject.authenticate_with_wordpress('user', 'pass')
      end

      it 'should store the credentials' do
        allow(subject).to receive(:wordpress_login).and_return('cookie')
        subject.authenticate_with_wordpress('user', 'pass')
        count = Wpxf::Models::Credential.count(username: 'user', password: 'pass')
        expect(count).to eql 1
      end

      it 'returns the cookie' do
        allow(subject).to receive(:wordpress_login).and_return('cookie')
        res = subject.authenticate_with_wordpress('user', 'pass')
        expect(res).to eq 'cookie'
      end
    end

    context 'when authentication fails' do
      it 'emits a message indicating the module failed to authenticate' do
        events_emitted = 1
        allow(subject).to receive(:on_event_emitted) do |event|
          if events_emitted == 2
            expect(event[:msg]).to match(/Failed to authenticate/)
            expect(event[:event]).to eq :output
            expect(event[:type]).to eq :error
          end

          events_emitted += 1
        end

        allow(subject).to receive(:wordpress_login).and_return(nil)
        subject.event_emitter.subscribe(subject)
        subject.authenticate_with_wordpress('user', 'pass')
      end

      it 'returns false' do
        allow(subject).to receive(:wordpress_login).and_return(nil)
        res = subject.authenticate_with_wordpress('user', 'pass')
        expect(res).to be false
      end
    end
  end

  describe '#can_execute?' do
    it 'returns true if all module and payload options are valid' do
      allow(subject).to receive(:aux_module?).and_return(false)
      subject.payload = Wpxf::Payload.new

      subject.options.each do |opt|
        subject.set_option_value(opt.name, '0')
      end

      expect(subject.can_execute?).to be true
    end

    it 'returns false if one or more module or payload options are invalid' do
      allow(subject).to receive(:aux_module?).and_return(false)
      subject.payload = Wpxf::Payload.new

      subject.payload.register_option(
        Wpxf::StringOption.new(name: 'a', desc: 'a', required: true)
      )

      subject.options.each do |opt|
        subject.set_option_value(opt.name, '0')
      end

      # Module options set and payload options not set
      expect(subject.can_execute?).to be false

      subject.payload.unset_option('a')
      subject.options.each do |opt|
        subject.unset_option(opt.name)
      end

      # Payload options set and module options not set
      expect(subject.can_execute?).to be false
    end
  end

  describe '#check_wordpress_and_online' do
    context 'when the target is running WordPress' do
      it 'returns true' do
        allow(subject).to receive(:wordpress_and_online?).and_return(true)
        expect(subject.check_wordpress_and_online).to be true
      end
    end

    context 'when the target is not running WordPress or is offline' do
      it 'emits a message indicating the target isn\'t running WordPress' do
        subject.event_emitter.subscribe(subject)
        allow(subject).to receive(:on_event_emitted) do |event|
          expect(event[:msg]).to match(/does not appear to be running/)
          expect(event[:event]).to eq :output
          expect(event[:type]).to eq :error
        end

        allow(subject).to receive(:wordpress_and_online?).and_return(false)
        subject.check_wordpress_and_online
      end

      it 'returns false' do
        allow(subject).to receive(:wordpress_and_online?).and_return(false)
        expect(subject.check_wordpress_and_online).to be false
      end
    end
  end

  describe '#run' do
    context 'when the check_wordpress_and_online option is set to true' do
      it 'returns false if the target is not online or not running WordPress' do
        allow(subject).to receive(:check_wordpress_and_online).and_return(false)
        subject.set_option_value('check_wordpress_and_online', true)
        expect(subject.run).to be false
      end

      it 'returns true if the target is running WordPress and no more checks are required' do
        allow(subject).to receive(:check_wordpress_and_online).and_return(true)
        expect(subject.run).to be true
      end
    end

    context 'when the requires_authentication setting is set to true' do
      it 'returns false if the target could not be authenticated with' do
        allow(subject_with_auth).to receive(:authenticate_with_wordpress).and_return(false)
        subject_with_auth.set_option_value('check_wordpress_and_online', false)
        expect(subject_with_auth.run).to be false
      end

      it 'returns true if authentication is successful' do
        allow(subject_with_auth).to receive(:authenticate_with_wordpress).and_return(true)
        subject_with_auth.set_option_value('check_wordpress_and_online', false)
        expect(subject_with_auth.run).to be true
      end
    end
  end
end
